﻿/*
 * Copyright © 2010-2011 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

using System.Reflection;
using Microsoft.Extensions.Logging;
using Spring.Context.Attributes.TypeFilters;
using Spring.Util;
using Spring.Objects.Factory.Xml;

namespace Spring.Context.Attributes;

/// <summary>
/// Scans Assebmlies for Types that satisfy a given set of constraints.
/// </summary>
[Serializable]
public abstract class AssemblyTypeScanner : IAssemblyTypeScanner
{
    /// <summary>
    /// Logger Instance.
    /// </summary>
    protected static readonly ILogger<AssemblyTypeScanner> Logger = LogManager.GetLogger<AssemblyTypeScanner>();

    /// <summary>
    /// Names of Assemblies to exclude from being loaded for scanning.
    /// </summary>
    protected IList<Func<string, bool>> AssemblyLoadExclusionPredicates = new List<Func<string, bool>>();

    /// <summary>
    /// Names of Assemblies to include for scanning.
    /// </summary>
    protected IList<Func<string, bool>> AssemblyLoadInclusionPredicates = new List<Func<string, bool>>();

    /// <summary>
    /// Assembly Inclusion Predicates.
    /// </summary>
    protected readonly List<Func<Assembly, bool>> AssemblyInclusionPredicates = new List<Func<Assembly, bool>>();

    /// <summary>
    /// Type Exclusion Predicates.
    /// </summary>
    protected readonly List<Func<Type, bool>> TypeExclusionPredicates = new List<Func<Type, bool>>();

    /// <summary>
    /// Type Exclusion Predicates.
    /// </summary>
    protected readonly List<ITypeFilter> TypeExclusionTypeFilters = new List<ITypeFilter>();

    /// <summary>
    /// Type Inclusion Predicates.
    /// </summary>
    protected readonly List<Func<Type, bool>> TypeInclusionPredicates = new List<Func<Type, bool>>();

    /// <summary>
    /// Type Inclusion TypeFilters.
    /// </summary>
    protected readonly List<ITypeFilter> TypeInclusionTypeFilter = new List<ITypeFilter>();

    /// <summary>
    /// Assemblies to scan.
    /// </summary>
    protected readonly List<IEnumerable<Type>> TypeSources = new List<IEnumerable<Type>>();

    /// <summary>
    /// Stores the object default definitons defined in the XML configuration documnet
    /// </summary>
    private DocumentDefaultsDefinition _defaults;

    private string _scanStartFolderPath;

    /// <summary>
    /// Stores the object default definitons defined in the XML configuration documnet
    /// </summary>
    public DocumentDefaultsDefinition Defaults
    {
        get { return _defaults; }
        set { _defaults = value; }
    }

    public string ScanStartFolderPath
    {
        get
        {
            //if we have no value, set it to the current bin dir
            if (string.IsNullOrEmpty(_scanStartFolderPath))
            {
                _scanStartFolderPath = GetCurrentBinDirectoryPath();
            }

            return _scanStartFolderPath;
        }
        set { _scanStartFolderPath = value; }
    }

    /// <summary>
    /// Assemblies the type of the having.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    public IAssemblyTypeScanner AssemblyHavingType<T>()
    {
        TypeSources.Add(new AssemblyTypeSource((typeof(T).Assembly)));
        return this;
    }

    /// <summary>
    /// Excludes the type.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    public IAssemblyTypeScanner ExcludeType<T>()
    {
        TypeExclusionPredicates.Add(t => t.FullName == typeof(T).FullName);
        return this;
    }

    /// <summary>
    /// Includes the type.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    public IAssemblyTypeScanner IncludeType<T>()
    {
        TypeInclusionPredicates.Add(t => t.FullName == typeof(T).FullName);
        return this;
    }

    /// <summary>
    /// Includes the types.
    /// </summary>
    /// <param name="typeSource">The type source.</param>
    /// <returns></returns>
    public IAssemblyTypeScanner IncludeTypes(IEnumerable<Type> typeSource)
    {
        AssertUtils.ArgumentNotNull(typeSource, "typeSource");
        TypeSources.Add(typeSource);
        TypeInclusionPredicates.Add(t => typeSource.Any(t1 => t1.FullName == t.FullName));
        return this;
    }

    /// <summary>
    /// Performs the Scan, respecting all filter settings.
    /// </summary>
    /// <returns></returns>
    public virtual IEnumerable<Type> Scan()
    {
        SetDefaultFilters();

        IList<Type> types = new List<Type>();

        foreach (Assembly assembly in GetAllMatchingAssemblies(ScanStartFolderPath))
        {
            TypeSources.Add(new AssemblyTypeSource(assembly));
        }

        foreach (var typeSource in TypeSources)
        {
            foreach (Type type in typeSource)
            {
                if (IsCompoundPredicateSatisfiedBy(type))
                {
                    if (Logger.IsEnabled(LogLevel.Debug))
                    {
                        Logger.LogDebug("Satisfied Type: {FullName}", type.FullName);
                    }

                    types.Add(type);
                }
            }
        }

        return types;
    }

    /// <summary>
    /// Adds the assembly filter.
    /// </summary>
    /// <param name="assemblyPredicate">The assembly predicate.</param>
    /// <returns></returns>
    public IAssemblyTypeScanner WithAssemblyFilter(Func<Assembly, bool> assemblyPredicate)
    {
        AssemblyInclusionPredicates.Add(assemblyPredicate);
        return this;
    }

    /// <summary>
    /// Adds the exclude filter.
    /// </summary>
    /// <param name="predicate">The predicate.</param>
    /// <returns></returns>
    public IAssemblyTypeScanner WithExcludeFilter(Func<Type, bool> predicate)
    {
        TypeExclusionPredicates.Add(predicate);
        return this;
    }

    /// <summary>
    /// Adds the exclude filter.
    /// </summary>
    /// <param name="filter">The type filter.</param>
    /// <returns></returns>
    public IAssemblyTypeScanner WithExcludeFilter(ITypeFilter filter)
    {
        if (filter != null)
            TypeExclusionTypeFilters.Add(filter);

        return this;
    }

    /// <summary>
    /// Adds the include filter.
    /// </summary>
    /// <param name="predicate">The predicate.</param>
    /// <returns></returns>
    public IAssemblyTypeScanner WithIncludeFilter(Func<Type, bool> predicate)
    {
        TypeInclusionPredicates.Add(predicate);
        return this;
    }

    /// <summary>
    /// Adds the include filter.
    /// </summary>
    /// <param name="filter">The filter type.</param>
    /// <returns></returns>
    public IAssemblyTypeScanner WithIncludeFilter(ITypeFilter filter)
    {
        if (filter != null)
            TypeInclusionTypeFilter.Add(filter);

        return this;
    }

    private List<string> GetAllAssembliesInPath(string folderPath)
    {
        var assemblies = new List<string>();
        assemblies.AddRange(DiscoverAssemblies(folderPath, "*.dll"));
        assemblies.AddRange(DiscoverAssemblies(folderPath, "*.exe"));

        if (Logger.IsEnabled(LogLevel.Debug))
        {
            Logger.LogDebug("Assemblies to be scanned: {AssemblyNames}", StringUtils.ArrayToCommaDelimitedString(assemblies.ToArray()));
        }

        return assemblies;
    }

    private IEnumerable<Assembly> GetAllMatchingAssemblies(string folderPath)
    {
        IEnumerable<string> assemblyCandidates = GetAllAssembliesInPath(folderPath);

        IList<Assembly> assemblies = new List<Assembly>();

        foreach (string assembly in assemblyCandidates)
        {
            if (!string.IsNullOrEmpty(assembly))
            {
                Assembly loadedAssembly = TryLoadAssemblyFromPath(assembly);

                if (null != loadedAssembly)
                {
                    string fullname = loadedAssembly.FullName;
                    Logger.LogDebug("Add Assembly: {FullName}", fullname);

                    assemblies.Add(loadedAssembly);
                }
            }
        }

        return ApplyAssemblyFiltersTo(assemblies);
    }

    private Assembly TryLoadAssemblyFromPath(string filename)
    {
        Assembly assembly = null;

        try
        {
            assembly = Assembly.LoadFrom(filename);
        }
        catch (Exception ex)
        {
            //log and swallow everything that might go wrong here...
            Logger.LogDebug(ex, "Failed to load assembly {FileName} to inspect for [Configuration] types!", filename);
        }

        return assembly;
    }

    private string GetCurrentBinDirectoryPath()
    {
        return string.IsNullOrEmpty(AppDomain.CurrentDomain.DynamicDirectory)
            ? AppDomain.CurrentDomain.BaseDirectory
            : AppDomain.CurrentDomain.DynamicDirectory;
    }

    /// <summary>
    /// Applies the assembly filters to the assembly candidates.
    /// </summary>
    /// <param name="assemblyCandidates">The assembly candidates.</param>
    /// <returns></returns>
    protected virtual IEnumerable<Assembly> ApplyAssemblyFiltersTo(IEnumerable<Assembly> assemblyCandidates)
    {
        var filteredAssemblies = assemblyCandidates.Where(IsIncludedAssembly).ToList();

        if (Logger.IsEnabled(LogLevel.Debug))
        {
            Logger.LogDebug("Filtered Assemblies: {0}", StringUtils.ArrayToCommaDelimitedString(filteredAssemblies));
        }

        return filteredAssemblies;
    }

    /// <summary>
    /// Determines whether the compound predicate is satisfied by the specified type.
    /// </summary>
    /// <param name="type">The type.</param>
    /// <returns>
    /// 	<c>true</c> if the compound predicate is satisfied by the specified type; otherwise, <c>false</c>.
    /// </returns>
    protected abstract bool IsCompoundPredicateSatisfiedBy(Type type);

    /// <summary>
    /// Determines whether [is excluded type] [the specified type].
    /// </summary>
    /// <param name="type">The type.</param>
    /// <returns>
    /// 	<c>true</c> if [is excluded type] [the specified type]; otherwise, <c>false</c>.
    /// </returns>
    protected virtual bool IsExcludedType(Type type)
    {
        if (TypeExclusionPredicates.Count > 0 && TypeExclusionPredicates.Any(exclude => exclude(type)))
            return true;

        return Enumerable.Any(TypeExclusionTypeFilters, filter => filter.Match(type));
    }

    /// <summary>
    /// Determines whether [is included assembly] [the specified assembly].
    /// </summary>
    /// <param name="assembly">The assembly.</param>
    /// <returns>
    /// 	<c>true</c> if [is included assembly] [the specified assembly]; otherwise, <c>false</c>.
    /// </returns>
    protected virtual bool IsIncludedAssembly(Assembly assembly)
    {
        foreach (var include in AssemblyInclusionPredicates)
        {
            if (include(assembly))
            {
                if (Logger.IsEnabled(LogLevel.Debug))
                {
                    Logger.LogDebug("Include Assembly: {FullName}", assembly.FullName);
                }

                return true;
            }
        }

        return false;
    }

    /// <summary>
    /// Determines whether [is included type] [the specified type].
    /// </summary>
    /// <param name="type">The type.</param>
    /// <returns>
    /// 	<c>true</c> if [is included type] [the specified type]; otherwise, <c>false</c>.
    /// </returns>
    protected virtual bool IsIncludedType(Type type)
    {
        for (var i = 0; i < TypeInclusionPredicates.Count; i++)
        {
            var include = TypeInclusionPredicates[i];
            if (include(type))
            {
                return true;
            }
        }

        for (var i = 0; i < TypeInclusionTypeFilter.Count; i++)
        {
            var filter = TypeInclusionTypeFilter[i];
            if (filter.Match(type))
            {
                return true;
            }
        }

        return false;
    }

    /// <summary>
    /// Sets the default filters.
    /// </summary>
    protected virtual void SetDefaultFilters()
    {
        if (TypeInclusionPredicates.Count == 0 && TypeInclusionTypeFilter.Count == 0)
            TypeInclusionPredicates.Add(obj => true);

        if (TypeExclusionPredicates.Count == 0 && TypeExclusionTypeFilters.Count == 0)
            TypeExclusionPredicates.Add(obj => false);

        if (AssemblyInclusionPredicates.Count == 0)
            AssemblyInclusionPredicates.Add(obj => true);
    }

    /// <summary>
    /// Loads the assemblies found.
    /// </summary>
    /// <param name="folderPath">The folder path.</param>
    /// <param name="extension">The extension.</param>
    private IList<string> DiscoverAssemblies(string folderPath, string extension)
    {
        IList<string> assemblies = new List<string>();

        string[] files = Directory.GetFiles(folderPath, extension, SearchOption.AllDirectories);

        foreach (string file in files)
        {
            string name = Path.GetFileNameWithoutExtension(file);

            bool isNotExcluded = !AssemblyLoadExclusionPredicates.Any(exclude => exclude(name));
            bool isIncluded = AssemblyLoadInclusionPredicates.Any(include => include(name));

            if (isNotExcluded || isIncluded)
            {
                assemblies.Add(file);
            }
        }

        return assemblies;
    }
}